/* Copyright 2009 Requirements Management System 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mags.remas.process;

import java.io.StringWriter;  
import java.sql.Blob;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.validator.GenericValidator;
import org.mags.common.EngineContext;
import org.mags.dao.common.ESortOrder;
import org.mags.remas.control.commons.IStartClass;
import org.mags.remas.exception.SingletonExceptionHandler;
import org.mags.remas.freemarker.BufferedTextTemplateLoader;
import org.mags.remas.logic.EntityFileUtils;
import org.mags.remas.logic.EntityLogic;
import org.mags.remas.model.commons.AuditInterceptor;
import org.mags.remas.model.entity.Entity;
import org.mags.remas.model.entity.EntityCategory;
import org.mags.remas.model.entity.EntityFile;
import org.mags.remas.model.entity.EntityReference;
import org.mags.remas.model.entity.EntityRelationship;
import org.mags.remas.model.entity.EntityType;

import freemarker.cache.TemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateMethodModel;
import freemarker.template.TemplateModelException;

/**
 * 
 * @author Mario Gaitan
 *
 */
public class FreeMarkerProcess implements IStartClass {
	private List<Integer> templateIds;
	private EntityRelationship document;
	private Map<String, Object> cacheModel=new HashMap<String, Object>();
	private Map<String, Object> cacheReferences=new HashMap<String, Object>();
	private List<Integer> allTypeIds = null;
	private int templatesNotAccepted = 0;
	private static final int MAX_TEMPLATES_NOT_ACCEPTED=50;
	private static final List<Object> EMPTY_LIST = new ArrayList<Object>();
	boolean error = false;
	public void findTemplates(EntityRelationship entityRelationship, Map<Long, Object> templates) {
		List<EntityRelationship> children = EntityLogic.getInstance().getEntityChildren(entityRelationship, templateIds);
		for(EntityRelationship i: children) {
			if(!error) {
				if(templates.get(i.getEntityRelationshipId())==null)
					templates.put(i.getEntityRelationshipId(), i);
				else if(templatesNotAccepted<=MAX_TEMPLATES_NOT_ACCEPTED)
					templatesNotAccepted++;
				else if(templatesNotAccepted>MAX_TEMPLATES_NOT_ACCEPTED) {
					SingletonExceptionHandler.getInstance().showError("dead lock process");
					error=true;
				}
				if(!error)
					findTemplates(i.getLink(), templates);
			}
		}
	}
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void execute(Object context) {
		Map<String, Object> _context = (Map<String, Object>)context;
		document = (EntityRelationship)_context.get("entityRelationship");
		templateIds = new ArrayList<Integer>();
		EntityType templateET = EngineContext.getDataContext().getEntityProviderFactory().getEntityTypeProvider().getByCode("TEMPL", document.getEntity().getEntityType().getEntityCategory().getEntityCategoryId());
		templateIds.add(templateET.getEntityTypeId());
		EntityRelationship mainTemplate = document.getLink();
		Map<Long, Object> templates = new HashMap<Long, Object>();
		cacheModel.clear();
		cacheReferences.clear();
		findTemplates(mainTemplate, templates);
		List<Long> ids= new ArrayList<Long>();
		ids.add(mainTemplate.getEntity().getEntityId());
		for(Long t: templates.keySet())
			ids.add(((EntityRelationship)templates.get(t)).getLink().getEntity().getEntityId());
		Map<String, String> templateContents = new HashMap<String, String>();
		List<EntityFile> files = EntityFileUtils.getFiles(ids.toArray());
		for(EntityFile file: files) {
			String text = new String(file.getRawContent());
			boolean loaded = false;
			for(Long t: templates.keySet()) {
				EntityRelationship link = (EntityRelationship)templates.get(t);
				if(link.getLink().getEntity().getEntityId()==file.getEntity().getEntityId()) {
					templateContents.put(link.getEntity().getName(), text);
					loaded = true;
					break;
				}
			}
			if(!loaded&&file.getEntity().getEntityId()==mainTemplate.getEntity().getEntityId())
				templateContents.put(mainTemplate.getEntity().getName(), text);
			
		}
		
		Configuration configuration = new Configuration();
		TemplateLoader templateLoader = new BufferedTextTemplateLoader(templateContents);
		configuration.setTemplateLoader(templateLoader);
		configuration.setObjectWrapper(new DefaultObjectWrapper());
		StringWriter out = new StringWriter();
		try {
			Map entityContext = makeEntityContext();
			Template temp = configuration.getTemplate(mainTemplate.getEntity().getName());
			temp.process(entityContext, out);
			List<EntityFile> entityFiles = EngineContext.getDataContext().getEntityProviderFactory().getEntityFileProvider().getByEntityId(document.getEntity().getEntityId());
			EntityFile entityFile;
			if(entityFiles.size()>0)
				entityFile = entityFiles.get(0);
			else {
				entityFile = new EntityFile();
				entityFile.setEntity(document.getEntity());
			}
			Blob blob = EngineContext.getDataContext().getEntityProviderFactory().getEntityFileProvider().getDao().createBlob(out.getBuffer().toString().getBytes());
			entityFile.setContent(blob);
			EngineContext.getDataContext().getEntityProviderFactory().getEntityFileProvider().createOrUpdate(entityFile);
		} catch (Exception e) {
			if(e instanceof RuntimeException)
				throw (RuntimeException)e;
			else
				throw new RuntimeException(e);
		} finally {
			out.flush();
		}
	}
	private void loadChildren(EntityRelationship parent, List<EntityRelationship> newChilds, List<EntityRelationship> childs) {
		for(EntityRelationship c: childs) 
			if(c.getEntityParent().getEntityId()==parent.getEntity().getEntityId()) {
				newChilds.add(c);
				loadChildren(c, newChilds, childs);
			}
	}
	private List<Integer> getAllTypeIds(EntityCategory entityCategory) {
		if(allTypeIds==null) {
			allTypeIds = new ArrayList<Integer>();
			List<EntityType> allowedTypes = EngineContext.getDataContext().getEntityProviderFactory().getEntityTypeProvider().getByCategoryId(entityCategory.getEntityCategoryId());
			for(EntityType i: allowedTypes) 
				allTypeIds.add(i.getEntityTypeId());
		}
		return allTypeIds;
	}
	private List<Integer> getEntityTypeIds(String types, EntityCategory entityCategory) {
		if(types.equals("*")) {
			return getAllTypeIds(entityCategory);
		} else {
			List<Integer> entityTypeIds = new ArrayList<Integer>();
			String[] typesArray = types.split(",");
			for(String type: typesArray) {
				EntityType entityType = EngineContext.getDataContext().getEntityProviderFactory().getEntityTypeProvider().getByCode(type.trim(), entityCategory.getEntityCategoryId());
				if(entityType==null)
					throw new RuntimeException("entity type not found");
				entityTypeIds.add(entityType.getEntityTypeId());
			}
			return entityTypeIds;
		}
	}
	@SuppressWarnings("unchecked")
	private List<EntityRelationship> loadCache(String cacheName, List<EntityRelationship> objects) {
		List<EntityRelationship> cacheObjects = (List<EntityRelationship>)cacheModel.get(cacheName);
		if(cacheObjects==null) {
			cacheObjects=new ArrayList<EntityRelationship>();
			cacheModel.put(cacheName, cacheObjects);
		}
		if(objects!=null&&objects.size()>0) {
			boolean f = false;
			for(EntityRelationship o: objects) {
				f = false;
				for(EntityRelationship co: cacheObjects)  {
					if(co.getEntity().getEntityId()==o.getEntity().getEntityId()
					&& co.getEntityParent().getEntityId()==o.getEntityParent().getEntityId()) {
						f = true;
						break;
					}
				}
				if(!f)
					cacheObjects.add(o);
			}
		}
		return cacheObjects;		
	}
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private Map makeEntityContext() {
		final Map entityContext = new HashMap();
		entityContext.put("this", document);
		entityContext.put("escapeHtml", new TemplateMethodModel() {
			@Override
			public Object exec(List arg0) throws TemplateModelException {
				if(arg0.size()>0&&arg0.get(0)!=null&&arg0.get(0) instanceof String) {
					String text = (String)arg0.get(0);
					return StringEscapeUtils.escapeHtml(text);
				}
				return "";
			}
		});				
		entityContext.put("getCurrentUser", new TemplateMethodModel() {
			@Override
			public Object exec(List arg0) throws TemplateModelException {
				return AuditInterceptor.getInstance().getUserName();
			}
		});
		entityContext.put("getCurrentTime", new TemplateMethodModel() {
			@Override
			public Object exec(List arg0) throws TemplateModelException {
				return new Date();
			}
		});		
		entityContext.put("getCurrentDate", new TemplateMethodModel() {
			@Override
			public Object exec(List arg0) throws TemplateModelException {
				return new Date();
			}
		});
		entityContext.put("getDescendants", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String hierarchicalPath = (String)args.get(0);
				String types = (String)args.get(1);
				String category = (String)args.get(2);
				Boolean loadKeys = Boolean.TRUE;
				if(args.size()>3)
					loadKeys = Boolean.parseBoolean((String)args.get(3));
				String[][] orderBy = new String[][]{{"position", ESortOrder.dsc.name()}};
				EntityCategory entityCategory = EngineContext.getDataContext().getEntityProviderFactory().getEntityCategoryProvider().getByCode(category.trim());
				if(entityCategory==null)
					throw new RuntimeException("Entity category not found");
				List<Integer> entityTypeIds = getEntityTypeIds(types, entityCategory);
				List<EntityRelationship> children = EngineContext.getDataContext().getEntityProviderFactory().getEntityRelationshipProvider().getDescendants(hierarchicalPath, orderBy, entityTypeIds);				
				if(loadKeys)
					EntityLogic.getInstance().loadKeys(children.toArray());
				List<EntityRelationship> newChilds = new ArrayList<EntityRelationship>();
				boolean f = false;
				for(EntityRelationship c: children) {
					f = false;
					for(EntityRelationship c1: children) {
						if(c1.getEntity().getEntityId()==c.getEntityParent().getEntityId()) {
							f = true;
							break;
						}
					}
					if(!f) {
						newChilds.add(c);
						loadChildren(c, newChilds, children);
					}
				}
				return newChilds;
			}
		});		
		entityContext.put("loadDescendantsInCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				args.remove(0);
				TemplateMethodModel templateModel = (TemplateMethodModel)entityContext.get("getDescendants");
				List<EntityRelationship> objects = (List<EntityRelationship>)templateModel.exec(args);
				return loadCache(cacheName, objects);
			}
		});						
		entityContext.put("getRelationshipsByPath", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String hierarchicalPath = (String)args.get(0);
				String types = (String)args.get(1);
				String category = (String)args.get(2);
				Boolean loadKeys = Boolean.TRUE;
				if(args.size()>3)
					loadKeys = Boolean.parseBoolean((String)args.get(3));
				String[][] orderBy = new String[][]{{"position", ESortOrder.dsc.name()}};
				EntityCategory entityCategory = EngineContext.getDataContext().getEntityProviderFactory().getEntityCategoryProvider().getByCode(category.trim());
				if(entityCategory==null)
					throw new RuntimeException("Entity category not found");
				List<Integer> entityTypeIds = getEntityTypeIds(types, entityCategory);
				long entityParentId = 0;
				if(hierarchicalPath.equals("/")) {
					Entity root = EngineContext.getDataContext().getEntityProviderFactory().getEntityProvider().getROOT(entityCategory.getEntityCategoryId());
					entityParentId = root.getEntityId();
				} else {
					String[] ids = hierarchicalPath.split("/");
					entityParentId = Long.valueOf(ids[ids.length-1]);
				}
				List<EntityRelationship> children = EngineContext.getDataContext().getEntityProviderFactory().getEntityRelationshipProvider().getByIdAndPath(entityParentId, hierarchicalPath, orderBy, entityTypeIds);
				if(loadKeys)
					EntityLogic.getInstance().loadKeys(children.toArray());
				return children;
			}
		});		
		entityContext.put("loadRelationshipsByPathInCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				args.remove(0);
				TemplateMethodModel templateModel = (TemplateMethodModel)entityContext.get("getRelationshipsByPath");
				List<EntityRelationship> objects = (List<EntityRelationship>)templateModel.exec(args);
				return loadCache(cacheName, objects);
			}
		});	
		entityContext.put("getRelationships", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String grandPaCode = (String)args.get(0);
				String parentCode = (String)args.get(1);
				Entity grandPa = null;
				Entity parent = null;
				String types = (String)args.get(2);
				String category = (String)args.get(3);
				Boolean loadKeys = Boolean.TRUE;
				if(args.size()>4)
					loadKeys = Boolean.parseBoolean((String)args.get(4));
				String[][] orderBy = new String[][]{{"position", ESortOrder.dsc.name()}};
				EntityRelationship parentRelationship = null;
				EntityCategory entityCategory = EngineContext.getDataContext().getEntityProviderFactory().getEntityCategoryProvider().getByCode(category.trim());
				if(entityCategory==null)
					throw new RuntimeException("Entity category not found");
				if(grandPaCode.equalsIgnoreCase("ROOT"))
					grandPa = EngineContext.getDataContext().getEntityProviderFactory().getEntityProvider().getROOT(entityCategory.getEntityCategoryId());
				else if(!GenericValidator.isBlankOrNull(grandPaCode))
					grandPa = EngineContext.getDataContext().getEntityProviderFactory().getEntityProvider().getByCode(grandPaCode);
				if(parentCode.equalsIgnoreCase("ROOT"))
					parent = EngineContext.getDataContext().getEntityProviderFactory().getEntityProvider().getROOT(entityCategory.getEntityCategoryId());
				else if(!GenericValidator.isBlankOrNull(parentCode))	
					parent = EngineContext.getDataContext().getEntityProviderFactory().getEntityProvider().getByCode(parentCode);
				if(parent==null||(!GenericValidator.isBlankOrNull(grandPaCode)&&grandPa==null))
					throw new RuntimeException("entity is not found");
				if(grandPa!=null) {
					List<EntityRelationship> relations = EngineContext.getDataContext().getEntityProviderFactory().getEntityRelationshipProvider().getByEntityIds(parent.getEntityId(), grandPa.getEntityId());
					if(relations.size()==0)
						throw new RuntimeException("relationship not found");
					parentRelationship = relations.get(0);
				}					
				if(parentRelationship==null){
					parentRelationship = new EntityRelationship();
					parentRelationship.setEntity(parent);
					parentRelationship.setHierarchicalPath("/");
					parentRelationship.setEntityType(parent.getEntityType());
				}
				List<Integer> entityTypeIds = getEntityTypeIds(types, entityCategory);
				List<EntityRelationship> children = EngineContext.getDataContext().getEntityProviderFactory().getEntityRelationshipProvider().getByParent(parentRelationship, orderBy, entityTypeIds);
				if(loadKeys)
					EntityLogic.getInstance().loadKeys(children.toArray());
				return children;
			}
		});
		entityContext.put("loadRelationshipsInCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				args.remove(0);
				TemplateMethodModel templateModel = (TemplateMethodModel)entityContext.get("getRelationships");
				List<EntityRelationship> objects = (List<EntityRelationship>)templateModel.exec(args);
				return loadCache(cacheName, objects);
			}
		});		
		entityContext.put("removeFromCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				Object object = cacheModel.get(cacheName);
				cacheModel.remove(cacheName);
				return (object!=null)?object:EMPTY_LIST;
			}
		});
		entityContext.put("getFromCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				return (cacheModel.get(cacheName)!=null)?cacheModel.get(cacheName):EMPTY_LIST;
			}
		});

		
		entityContext.put("getReferences", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String entityCode = (String)args.get(0);
				Entity entity = EngineContext.getDataContext().getEntityProviderFactory().getEntityProvider().getByCode(entityCode);
				List<EntityReference> references = EngineContext.getDataContext().getEntityProviderFactory().getEntityReferenceProvider().getByEntityId(entity.getEntityId());
				return references;
			}
		});
		entityContext.put("makeReferencesFromCacheRelations", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				List<EntityRelationship> relations = (List<EntityRelationship>)((cacheModel.get(cacheName)!=null)?cacheModel.get(cacheName):EMPTY_LIST);
				List<Long> ids = new ArrayList<Long>();
				for(EntityRelationship r: relations)
					ids.add(r.getEntity().getEntityId());
				List<EntityReference> references = EngineContext.getDataContext().getEntityProviderFactory().getEntityReferenceProvider().getByEntitiesId(ids.toArray());
				return references;
			}
		});
		entityContext.put("makeReferencesFromCacheRelationsAndLoadInCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				String fromCacheName = (String)args.get(1);
				List<EntityRelationship> relations = (List<EntityRelationship>)((cacheModel.get(cacheName)!=null)?cacheModel.get(cacheName):EMPTY_LIST);
				List<Long> ids = new ArrayList<Long>();
				for(EntityRelationship r: relations)
					ids.add(r.getEntity().getEntityId());
				List<EntityReference> references = EngineContext.getDataContext().getEntityProviderFactory().getEntityReferenceProvider().getByEntitiesId(ids.toArray());
				cacheReferences.put(fromCacheName, references);
				return references;
			}
		});
		entityContext.put("removeFromReferenceCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				Object object = cacheReferences.get(cacheName);
				cacheReferences.remove(cacheName);
				return (object!=null)?object:EMPTY_LIST;
			}
		});
		entityContext.put("getFromReferenceCache", new TemplateMethodModel() {
			@Override
			public Object exec(List args) throws TemplateModelException {
				String cacheName = (String)args.get(0);
				return (cacheReferences.get(cacheName)!=null)?cacheReferences.get(cacheName):EMPTY_LIST;
			}
		});		
		return entityContext;
	}
}
